/**
 * Copyright 2011, Under-Ground Studio.
 *  
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *  
 *      http://www.apache.org/licenses/LICENSE-2.0
 *  
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package cn.edu.zju.vlis.aus.chart;

import java.util.ArrayList;
import java.util.List;

import android.app.Activity;
import android.content.Context;
import android.graphics.Bitmap;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.Path;
import android.graphics.Path.Direction;
import android.graphics.Rect;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.util.AttributeSet;
import android.util.DisplayMetrics;
import android.view.MotionEvent;
import android.view.View;
import cn.edu.zju.vlis.aus.R;

/**
 * Instance of this class represents a view that shows a pie chart. The view
 * includes two parts: 1.One rectangle to show a pie chart. 2.Other area show a
 * pop in which we display the detail data.
 * 
 * The operation you can do with the view includes: 1.You can select a pie to
 * see its detail data. 2.You can select and rotate the chart.
 * 
 * @author ymsong (songyuming1985@gmail.com) 2011-8-3
 */
public class PieChartView extends View implements ChartDataAdapterListener {
	private static final int TEXT_SIZE_BIG = 16;
	// size of staff.
	private float padding = 20;
	private float minPopHeight = 100;
	private float minPopWidth = 250;
	private float triangleWidth = 30;
	private float triangleHeight = 30;
	private float roundRadius = 12;
	private float shadowInsetForPie = 3;
	private float shadowInsetForPop = 5;
	private DisplayMetrics metrics;
	// state constants
	private static final int STATE_NOT_DRAWN = 0;
	private static final int STATE_DRAWING = 1;
	private static final int STATE_DRAWN = 2;
	// the state of this view.
	private int drawState;
	private float currentDrawAngle;
	// data and data cache
	private ChartDataAdapter adapter;
	private List<PieChartDataItem> adapterCache;

	// cache for rectangle and other things.
	private RectF pieChartRect;// chart rectangle
	private RectF popRect;// the rectangle that display the pop
//	private boolean landscape;// is landscape

	// offset of the angle, range [0,360)
	private float angleOffset;

	// the selected item
	private PieChartDataItem selectedItem;
	private boolean selectionChange;

	// flag to indicate animation is going
	private boolean inAnimation;
	// cached images
	private Drawable pieChartMask;
	private Bitmap popBitmap;
	private Drawable popMask;
	private float scale;

	/**
	 * Default constructor.
	 * 
	 * @param context
	 *            the context.
	 */
	public PieChartView(Context context) {
		super(context);
		// setBackgroundColor(Color.WHITE);
		initVariables();
	}

	/**
	 * Constructor for XML inflater.
	 * 
	 * @param context
	 * @param attrs
	 */
	public PieChartView(Context context, AttributeSet attrs) {
		super(context, attrs);
		initVariables();
	}

	private void initVariables() {
		metrics = new DisplayMetrics();
		Activity activity = (Activity) getContext();
		activity.getWindowManager().getDefaultDisplay().getMetrics(metrics);
		padding *= metrics.scaledDensity;
		minPopHeight *= metrics.scaledDensity;
		minPopWidth *= metrics.scaledDensity;
		triangleWidth *= metrics.scaledDensity;
		triangleHeight *= metrics.scaledDensity;
		roundRadius *= metrics.scaledDensity;
		shadowInsetForPie *= metrics.scaledDensity;
		shadowInsetForPop *= metrics.scaledDensity;
		setPadding((int) padding, (int) padding, (int) padding, (int) padding);
		drawState = STATE_NOT_DRAWN;
	}

	/**
	 * Perform the drawing things.
	 */
	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);
		// calculate the cache.
		if (adapterCache == null && adapter != null) {
			calculateDataCache();
		}
		if (adapterCache.size() > 0) {
			if (pieChartRect == null || popRect == null) {
				calculateRectangles();
			}
			// set up a paint.
			Paint paint = new Paint();
			paint.setAntiAlias(true);
			paint.setStyle(Paint.Style.FILL);
			// draw the chart rectangle.
			if (drawState == STATE_NOT_DRAWN) {// first time, use animation.
				new Thread(new AnimDrawPieRunnable(), "Anim_Draw_Pie").start();
			} else if (drawState == STATE_DRAWING) {// in drawing
				drawPieChart(canvas, paint, pieChartRect, currentDrawAngle);
			} else if (drawState == STATE_DRAWN) {// draw directly
				drawPieChart(canvas, paint, pieChartRect, 360);
			}
			// draw the pop rectangle.
			if (selectedItem != null && scale > 0) {
				if (selectionChange) {
					popBitmap = createPopImage();
					selectionChange = false;
				}
				float width = scale * popRect.width();
				float height = scale * popRect.height();
				RectF dst = new RectF();
//				if (!landscape) {
				dst.top = popRect.top;
				dst.bottom = dst.top + height;
				dst.left = popRect.left + (popRect.width() - width) / 2;
				dst.right = popRect.left + (popRect.width() + width) / 2;
//				} else {
//					dst.top = popRect.top + (popRect.height() - height) / 2;
//					dst.bottom = popRect.top + (popRect.height() + height) / 2;
//					dst.left = popRect.left;
//					dst.right = popRect.left + width;
//				}
				canvas.drawBitmap(popBitmap, null, dst, null);
			}
		} else {// draw empty view in the center of view.
			Paint paint = new Paint();
			paint.setAntiAlias(true);
			paint.setTextSize(TEXT_SIZE_BIG * metrics.scaledDensity);
			String emptyText = getContext().getResources().getString(
					R.string.chart_no_data);
			Rect textBounds = new Rect();
			paint.getTextBounds(emptyText, 0, emptyText.length(), textBounds);
			int x = (getWidth() - textBounds.width()) / 2;
			int y = (getHeight() - textBounds.height()) / 2 + textBounds.top;
			canvas.drawText(emptyText, x, y, paint);
		}
	}

	private void calculateDataCache() {
		int count = adapter.getItemCount();
		adapterCache = new ArrayList<PieChartDataItem>(count);
		float totalNumber = 0;
		for (int i = 0; i < count; i++) {
			ChartDataItem item = adapter.getItem(i);
			if (item instanceof PieChartDataItem) {
				adapterCache.add((PieChartDataItem) item);
			}
			totalNumber += item.getNumber();
		}
		float angle = 0;
		for (PieChartDataItem pieItem : adapterCache) {
			pieItem.percentage = pieItem.getNumber() / totalNumber;
			pieItem.angleStart = angle;
			angle += pieItem.percentage * 360;
			pieItem.angleEnd = angle;
		}
	}

	private void calculateRectangles() {
		// Portrait mode
		int availableWidth = getWidth() - getPaddingRight() - getPaddingLeft();
		int availableHeight = getHeight() - getPaddingTop()
				- getPaddingBottom();
//		if (availableWidth < availableHeight) {
//			landscape = false;
		angleOffset = 90;
		float sideLength = availableWidth;
		float paddingLeft = getPaddingLeft();
		float popHeight = availableHeight - availableWidth;
		if (popHeight < minPopHeight) {
			popHeight = minPopHeight;
			sideLength = availableHeight - minPopHeight;
			paddingLeft = (getWidth() - sideLength) / 2;
		}
		pieChartRect = new RectF(paddingLeft, getPaddingTop(), paddingLeft
				+ sideLength, getPaddingTop() + sideLength);
		float popWidth = sideLength;
		if (popWidth < minPopWidth){
			popWidth = minPopWidth;
			paddingLeft = (getWidth() - popWidth) / 2;
		}
		popRect = new RectF(paddingLeft, getPaddingTop() + sideLength + 1,
				paddingLeft + popWidth, getPaddingTop() + availableHeight);
//		} else {// Landscape mode
//			landscape = true;
//			angleOffset = 0;
//			float sideLength = availableHeight;
//			float paddingTop = getPaddingTop();
//			float popWidth = availableWidth - availableHeight;
//			if (popWidth < minPopWidth) {
//				sideLength = availableWidth - minPopWidth;
//				paddingTop = (getHeight() - sideLength) / 2;
//			}
//			pieChartRect = new RectF(getPaddingLeft(), paddingTop,
//					getPaddingLeft() + sideLength, paddingTop + sideLength);
//			popRect = new RectF(getPaddingLeft() + sideLength + 1, paddingTop,
//					getPaddingLeft() + availableWidth, paddingTop + sideLength);
//		}
	}

	/*
	 * Draw a part of the pie chart.
	 */
	private void drawPieChart(Canvas canvas, Paint paint, RectF oval,
			float angle) {
		float start = angleOffset;
		// draw the pies
		if (selectedItem == null) {
			for (PieChartDataItem item : adapterCache) {
				// make sure not transparent color.
				int color = 0xFF000000 | item.getColor();
				float sweep = item.percentage * angle;
				paint.setColor(color);
				canvas.drawArc(oval, start, sweep, true, paint);
				start += sweep;
			}
		} else {// if there's selection, high light the selection.
			// draw a black background
			paint.setColor(Color.BLACK);
			canvas.drawOval(oval, paint);
			for (PieChartDataItem item : adapterCache) {
				int color = 0xFF000000 | item.getColor();
				if (item.selected) {
					paint.setColor(color);
				} else {
					color = Color.argb(100, Color.red(color),
							Color.green(color), Color.blue(color));
					paint.setColor(color);
				}
				float sweep = item.percentage * angle;
				canvas.drawArc(oval, start, sweep, true, paint);
				start += sweep;
			}
		}
		// draw mask
		if (pieChartMask == null) {
			pieChartMask = getResources().getDrawable(R.drawable.mask_piechart);
		}
		Rect maskRect = new Rect();
		pieChartRect.roundOut(maskRect);
		maskRect.right += shadowInsetForPie;
		maskRect.bottom += shadowInsetForPie;
		pieChartMask.setBounds(maskRect);
		pieChartMask.draw(canvas);
	}

	private Bitmap createPopImage() {
		int width = (int) (popRect.width() + shadowInsetForPop);
		int height = (int) (popRect.height() + shadowInsetForPop);
		Bitmap popImage = Bitmap.createBitmap(width, height,
				Bitmap.Config.ARGB_8888);
		Canvas canvas = new Canvas(popImage);
		Paint paint = new Paint();
		paint.setStyle(Paint.Style.FILL);
		paint.setAntiAlias(true);
		int pathWidth = (int) popRect.width();
		int pathHeight = (int) popRect.height();
		Path path = createPopPath(pathWidth, pathHeight);
		if (selectedItem != null) {
			paint.setColor(selectedItem.getColor());
			canvas.drawPath(path, paint);
			RectF clientArea = new RectF(0, 0, pathWidth, pathHeight);
//			if (!landscape) {
			clientArea.top += triangleHeight;
//			} else {
//				clientArea.left += triangleHeight;
//			}
			selectedItem.drawDetail(canvas, clientArea);
		}
		if (popMask == null) {
//			if (!landscape) {
			popMask = getResources().getDrawable(
					R.drawable.mask_pop_portrait);
//			} else {
//				popMask = getResources().getDrawable(
//						R.drawable.mask_pop_landscape);
//			}
		}
		popMask.setBounds(0, 0, width, height);
		popMask.draw(canvas);
		return popImage;
	}

	// get the shape of the pop rectangle.
	private Path createPopPath(int width, int height) {
		Path path = new Path();
		RectF popRect = new RectF(0, 0, width, height);
//		if (!landscape) {
		popRect.top += triangleHeight;
		// add a triangle at the top.
		path.moveTo(width / 2 - triangleWidth / 2, triangleHeight);
		path.lineTo(width / 2, 0);
		path.lineTo(width / 2 + triangleWidth / 2, triangleHeight);
		path.addRoundRect(popRect, roundRadius, roundRadius, Direction.CW);
//		} else {
//			popRect.left += triangleHeight;
//			// add a triangle at the left.
//			path.moveTo(triangleHeight, height / 2 - triangleWidth / 2);
//			path.lineTo(0, height / 2);
//			path.lineTo(triangleHeight, height / 2 + triangleWidth / 2);
//			path.addRoundRect(popRect, roundRadius, roundRadius, Direction.CCW);
//		}
		return path;
	}

	/**
	 * Perform the measuring.
	 */
	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		setMeasuredDimension(measureWidth(widthMeasureSpec),
				measureHeight(heightMeasureSpec));
	}

	// measure the width.
	private int measureWidth(int widthMeasureSpec) {
		int width = 400;
		int widthMode = MeasureSpec.getMode(widthMeasureSpec);
		int widthSize = MeasureSpec.getSize(widthMeasureSpec);
		if (widthMode == MeasureSpec.EXACTLY) {
			width = widthSize;
		}
		return width;
	}

	// measure the height
	private int measureHeight(int heightMeasureSpec) {
		int height = 0;
		int heightMode = MeasureSpec.getMode(heightMeasureSpec);
		int heightSize = MeasureSpec.getSize(heightMeasureSpec);
		if (heightMode == MeasureSpec.EXACTLY) {
			height = heightSize;
		} else if (heightMode == MeasureSpec.UNSPECIFIED) {
			height = getMeasuredWidth() * 3 / 2;
		} else if (heightMode == MeasureSpec.AT_MOST) {
			height = getMeasuredWidth() * 3 / 2;
			height = Math.min(height, heightSize);
		}
		return height;
	}

	private float lastX;
	private float lastY;
	private boolean hitChart;
	private boolean hitPop;

	/**
	 * Handling the touch event.
	 */
	@Override
	public boolean onTouchEvent(MotionEvent event) {
		if (adapterCache == null || adapterCache.size() == 0 || inAnimation) {
			return false;
		}
		float x = event.getX();
		float y = event.getY();
		int action = event.getAction();
		boolean handled = false;
		// handle the event by action.
		switch (action) {
		case MotionEvent.ACTION_DOWN:
			handled = handleTouchDown(x, y);
			break;
		case MotionEvent.ACTION_MOVE:
			handled = handleTouchMove(x, y);
			break;
		case MotionEvent.ACTION_UP:
			handled = handleTouchUp();
			break;
		}
		// track x,y
		lastX = x;
		lastY = y;
		return handled;
	}

	private boolean handleTouchDown(float x, float y) {
		boolean handled = false;
		if (pieChartRect.contains(x, y)) {
			float xDistance = x - pieChartRect.centerX();
			float yDistance = y - pieChartRect.centerY();
			float distance = (float) Math.sqrt(xDistance * xDistance
					+ yDistance * yDistance);
			if (distance <= pieChartRect.width() / 2) {
				hitChart = true;
			}
		}
		hitPop = popRect.contains(x, y);
		if (hitChart) {
			// calculate hit angle.
			double x0 = x - pieChartRect.centerX();
			double y0 = y - pieChartRect.centerY();
			double hitAngle = Math.atan2(y0, x0) * 180 / Math.PI;
			if (hitAngle < 0) {
				hitAngle += 360;
			}
			hitAngle -= angleOffset;
			if (hitAngle < 0) {
				hitAngle += 360;
			}
			// calculate the selected pie.
			for (PieChartDataItem item : adapterCache) {
				if (hitAngle >= item.angleStart && hitAngle < item.angleEnd) {
					if (selectedItem != null) {
						selectedItem.selected = false;
					}
					item.selected = true;
					selectedItem = item;
					break;
				}
			}
			scale = 0;
			handled = true;
		}
		if (!hitChart && !hitPop) {// cancel the selection
			if (selectedItem != null) {
				selectedItem.selected = false;
				selectedItem = null;
			}
			handled = false;
		}
		invalidate();
		return handled;
	}

	private boolean handleTouchMove(float x, float y) {
		if (hitChart) {
			double x1 = lastX - pieChartRect.centerX();
			double y1 = lastY - pieChartRect.centerY();
			double x2 = x - pieChartRect.centerX();
			double y2 = y - pieChartRect.centerY();
			double value = 0;
			if (x1 != 0 && x2 != 0) {
				double k1 = y1 / x1;
				double k2 = y2 / x2;
				value = (k2 - k1) / (1 + k1 * k2);
			} else if (x1 == 0 && x2 != 0) {
				value = -(x2 / y2);
			} else if (x1 != 0 && x2 == 0) {
				value = x1 / y1;
			}
			double offset = Math.atan(value) * 180 / Math.PI;
			if (offset != 0) {
				angleOffset += offset;
				if (angleOffset < 0) {
					angleOffset += 360;
				} else if (angleOffset >= 360) {
					angleOffset -= 360;
				}
				invalidateChartRect();
			}
			return true;
		} else {
			return false;
		}
	}

	private void invalidateChartRect() {
		Rect redrawArea = new Rect();
		pieChartRect.roundOut(redrawArea);
		invalidate(redrawArea);
	}

	private boolean handleTouchUp() {
		if (hitChart) {
			if (selectedItem != null) {// rotate the selection to right position
				makeSelection();
			}
			hitChart = false;
			return true;
		} else {
			return false;
		}
	}

	private void makeSelection() {
		selectionChange = true;
//		float initOffset = landscape ? 0 : 90;
		float initOffset = 90;
		float targetOffset = initOffset
				- (selectedItem.angleStart + selectedItem.angleEnd) / 2;
		if (targetOffset < 0) {
			targetOffset += 360;
		}
		new Thread(new AnimRotateAndPopRunnable(targetOffset),
				"Anim_Rotate_And_Pop").start();
	}

	@Override
	public void onDataChanged() {
		this.adapterCache = null;
		selectedItem = null;
		invalidate();
	}

	// programming interface

	/**
	 * Set the adapter of the view.
	 * 
	 * @param adapter
	 *            the adapter to set
	 */
	public void setAdapter(ChartDataAdapter adapter) {
		this.adapter = adapter;
		this.adapter.addListener(this);
	}

	/**
	 * Get the current selection. Return null if no selection.
	 * 
	 * @return selected data item.
	 */
	public ChartDataItem getSelection() {
		return selectedItem;
	}

	/**
	 * Set the selection. Set null to clear selection.
	 * 
	 * @param item
	 *            selection data.
	 */
	public void setSelection(ChartDataItem item) {
		if (adapterCache != null && adapterCache.size() > 0) {
			if (selectedItem != null && !selectedItem.equals(item)) {
				selectedItem.selected = false;
				selectedItem = null;
				invalidate();
			}
			if (item != null && !item.equals(selectedItem)) {
				int index = adapterCache.indexOf(item);
				if (index != -1) {
					selectedItem = adapterCache.get(index);
					selectedItem.selected = true;
					makeSelection();
				}
			}
		}
	}

	/**
	 * select to previous pie.
	 */
	public void selectPrev() {
		if (selectedItem != null) {
			int index = adapterCache.indexOf(selectedItem);
			if (index != -1) {
				if (index == 0) {
					int size = adapterCache.size();
					setSelection(adapterCache.get(size - 1));
				} else {
					setSelection(adapterCache.get(index - 1));
				}
			}

		} else if (adapterCache.size() > 0) {
			setSelection(adapterCache.get(0));
		}
	}

	/**
	 * select to next pie.
	 */
	public void selectNext() {
		if (selectedItem != null) {
			int index = adapterCache.indexOf(selectedItem);
			if (index != -1) {
				if (index == adapterCache.size() - 1) {
					setSelection(adapterCache.get(0));
				} else {
					setSelection(adapterCache.get(index + 1));
				}
			}
		} else if (adapterCache.size() > 0) {
			setSelection(adapterCache.get(0));
		}
	}

	/**
	 * The drawing animation runnable.
	 */
	private class AnimDrawPieRunnable implements Runnable {

		@Override
		public void run() {
			inAnimation = true;
			drawState = STATE_DRAWING;
			while (currentDrawAngle < 360) {
				currentDrawAngle += 1;
				Rect redrawArea = new Rect();
				pieChartRect.roundOut(redrawArea);
				postInvalidate(redrawArea.left, redrawArea.top,
						redrawArea.right, redrawArea.bottom);
				try {
					Thread.sleep(2);
				} catch (InterruptedException e) {
					// do nothing.
				}
			}
			drawState = STATE_DRAWN;
			try {
				Thread.sleep(100);
			} catch (InterruptedException e) {
			}
			inAnimation = false;
			// select first pie.
			if (adapter.getItemCount() > 0) {
				ChartDataItem item = adapter.getItem(0);
				setSelection(item);
			}
		}
	}

	/**
	 * The selection animation runnable.
	 */
	private class AnimRotateAndPopRunnable implements Runnable {
		private float targetOffset;

		private AnimRotateAndPopRunnable(float targetOffset) {
			this.targetOffset = targetOffset;
		}

		@Override
		public void run() {
			inAnimation = true;
			postInvalidate();
			// rotate to target angle.
			if (Math.abs(targetOffset - angleOffset) > 180) {
				if (targetOffset > angleOffset) {
					angleOffset += 360;
				} else {
					angleOffset -= 360;
				}
			}
			while (Math.abs(targetOffset - angleOffset) >= 1) {
				if (angleOffset < targetOffset) {
					angleOffset += 1;
				} else {
					angleOffset -= 1;
				}
				postInvalidateChartRect();
				try {
					Thread.sleep(1);
				} catch (InterruptedException e) {
					// do nothing
				}
			}
			angleOffset = targetOffset;
			postInvalidateChartRect();
			// pop up detail.
			scale = 0;
			while (scale <= 1.0) {
				postInvalidatePopRect();
				scale += 0.05;
				try {
					Thread.sleep(5);
				} catch (InterruptedException e) {
					// do nothing
				}
			}
			inAnimation = false;
		}

		private void postInvalidatePopRect() {
			Rect redrawArea = new Rect();
			popRect.roundOut(redrawArea);
			postInvalidate(redrawArea.left, redrawArea.top, redrawArea.right,
					redrawArea.bottom);
		}

		private void postInvalidateChartRect() {
			Rect redrawArea = new Rect();
			pieChartRect.roundOut(redrawArea);
			postInvalidate(redrawArea.left, redrawArea.top, redrawArea.right,
					redrawArea.bottom);
		}
	}
}
